# -*- coding: UTF-8 -*-
"""
Serveur BaseHTTPServer qui implémente le protocole Python WSGI (PEP 333,
révision 1.21).

Ce serveur est une adaptation de wsgiref.simple_server :
http://svn.eby-sarna.com/wsgiref/

C'est un serveur simple, à utiliser pour tester ou déboguer des applications
Django. Il n'a pas été testé au niveau de la sécurité. Ne l'utilisez pas pour
héberger votre site actif.
"""

from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from types import ListType, StringType, TupleType
import os, re, sys, time, urllib

__version__ = "0.1"
__all__ = ['WSGIServer','WSGIRequestHandler','demo_app']

server_version = "WSGIServer/" + __version__
sys_version = "Python/" + sys.version.split()[0]
software_version = server_version + ' ' + sys_version

class WSGIServerException(Exception):
    pass

class FileWrapper:
    """Une classe permettant de convertir les objets présentant une interface
    semblable aux objets fichier en objets itérables."""

    def __init__(self, filelike, blksize=8192):
        self.filelike = filelike
        self.blksize = blksize
        if hasattr(filelike,'close'):
            self.close = filelike.close

    def __getitem__(self,key):
        data = self.filelike.read(self.blksize)
        if data:
            return data
        raise IndexError

    def __iter__(self):
        return self

    def next(self):
        data = self.filelike.read(self.blksize)
        if data:
            return data
        raise StopIteration

# Une expression régulière qui correspond aux caractères `spéciaux' que l'on
# peut trouver dans les paramètres et dont l'existence au sein de la valeur
# d'un paramètre signifie que ce paramètre a été échappé.
tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')

def _formatparam(param, value=None, quote=1):
    """Fonction pratique permettant de créer et renvoyer une paire de la forme
    clé=valeur.

    Cette fonction échappera la valeur si nécessaire (ou si le flag quote est
    actif).
    """
    if value is not None and len(value) > 0:
        if quote or tspecials.search(value):
            value = value.replace('\\', '\\\\').replace('"', r'\"')
            return '%s="%s"' % (param, value)
        else:
            return '%s=%s' % (param, value)
    else:
        return param

class Headers:
    """Gère les en-têtes d'une réponse HTTP."""
    def __init__(self,headers):
        if type(headers) is not ListType:
            raise TypeError("L'argument passé au constructeur doit être une liste de tuples nom/valeur.")
        self._headers = headers

    def __len__(self):
        """Renvoie le nombre total d'en-têtes; les en-têtes présents en double
        sont comptés deux fois."""
        return len(self._headers)

    def __setitem__(self, name, val):
        """Définit la valeur d'un en-tête."""
        del self[name]
        self._headers.append((name, val))

    def __delitem__(self,name):
        """Supprime toutes les occurences de l'en-tête spécifié, si celui-ci
        est présent.

        Ne déclenche *pas* d'exception si l'en-tête est absent.
        """
        name = name.lower()
        self._headers[:] = [kv for kv in self._headers if kv[0].lower()<>name]

    def __getitem__(self,name):
        """Renvoie la première valeur de la liste correspondant à l'en-tête
        nommé 'name'.

        Renvoie None si l'en-tête est absent de la liste (ne déclenche pas
        d'exception).

        Notez que si l'en-tête spécifié apparaissait plusieurs fois dans le
        message HTTP, la valeur renvoyée est indéfinie. Vous pouvez utiliser
        getall() pour obtenir toutes les valeurs correspondant à un en-tête
        donné.
        """
        return self.get(name)

    def has_key(self, name):
        """Renvoie True si l'en-tête spécifié est présent."""
        return self.get(name) is not None

    __contains__ = has_key

    def get_all(self, name):
        """Renvoie une liste de toutes les valeurs correspondant à l'en-tête
        spécifié.

        Celles-ci seront triées par ordre d'apparition dans la liste d'en-têtes
        de départ ou par ordre d'ajout à l'instance; des valeurs en double sont
        possibles. Tout en-tête supprimé puis inséré est ajouté à la fin de la
        liste d'en-têtes. Si aucun en-tête dont le nom est 'name' n'existe,
        renvoie une liste vide.
        """
        name = name.lower()
        return [kv[1] for kv in self._headers if kv[0].lower()==name]


    def get(self,name,default=None):
        """Renvoie la première valeur de la liste correspondant à l'en-tête
        nommé 'name' ou renvoie default si l'en-tête est introuvable."""
        name = name.lower()
        for k,v in self._headers:
            if k.lower()==name:
                return v
        return default

    def keys(self):
        """Renvoie une liste des noms de tous les en-têtes.

        Ceux-ci seront triés par ordre d'apparition dans la liste d'en-têtes de
        départ ou par ordre d'ajout à l'instance; des valeurs en double sont
        possibles. Tout en-tête supprimé puis inséré est ajouté à la fin de la
        liste d'en-têtes.
        """
        return [k for k, v in self._headers]

    def values(self):
        """Renvoie une liste des valeurs de tous les en-têtes.

        Celles-ci seront triées par ordre d'apparition dans la liste d'en-têtes
        de départ ou par ordre d'ajout à l'instance; des valeurs en double sont
        possibles. Tout en-tête supprimé puis inséré est ajouté à la fin de la
        liste d'en-têtes.
        """
        return [v for k, v in self._headers]

    def items(self):
        """Renvoie une liste de tuples (nom, valeur) correspondant à chaque
        en-tête.

        Ceux-ci seront triés par ordre d'apparition dans la liste d'en-têtes de
        départ ou par ordre d'ajout à l'instance; des valeurs en double sont
        possibles. Tout en-tête supprimé puis inséré est ajouté à la fin de la
        liste d'en-têtes.
        """
        return self._headers[:]

    def __repr__(self):
        return "En-têtes (%s)" % `self._headers`

    def __str__(self):
        """Renvoie les en-têtes sous forme d'une chaîne complète pouvant être
        incluse au sein d'une transmission HTTP."""
        return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['',''])

    def setdefault(self,name,value):
        """Renvoie la première valeur de la liste correspondant à l'en-tête
        nommé 'name' ou renvoie 'value' si l'en-tête est introuvable.

        S'il n'y a pas d'en-tête nommé 'name', on ajoute un nouvel en-tête
        ayant pour nom 'name' et pour valeur 'value'."""
        result = self.get(name)
        if result is None:
            self._headers.append((name,value))
            return value
        else:
            return result

    def add_header(self, _name, _value, **_params):
        """Cette méthode permet de définir précisément un en-tête.

        _name constitue le nom de l'en-tête à ajouter. Des arguments passés par
        mot-clé peuvent en plus être utilisés pour définir des paramètres
        supplémentaires à inclure dans l'en-tête; les tirets bas des clés seront
        convertis en tirets. Généralement le paramètre sera ajouté sous la
        forme clé="valeur" sauf si la valeur est None, auquel cas seule la clé
        est ajoutée en tant que paramètre.

        Exemple :

        h.add_header('content-disposition', 'attachment', filename='bud.gif')

        Notez que contrairement à la méthode correspondante de la classe
        'email.Message', cette méthode ne gère *pas* les tuples
        '(jeu_de_caractères, langue, valeur)' : les valeurs passées à cette
        méthode doivent être des chaînes ou None.
        """
        parts = []
        if _value is not None:
            parts.append(_value)
        for k, v in _params.items():
            if v is None:
                parts.append(k.replace('_', '-'))
            else:
                parts.append(_formatparam(k.replace('_', '-'), v))
        self._headers.append((_name, "; ".join(parts)))

def guess_scheme(environ):
    """Essaie de deviner si 'wsgi.url_scheme' doit être 'http' ou 'https'."""
    if environ.get("HTTPS") in ('yes','on','1'):
        return 'https'
    else:
        return 'http'

_hoppish = {
    'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
    'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
    'upgrade':1
}.has_key

def is_hop_by_hop(header_name):
    """Renvoie True si 'header_name' est un en-tête HTTP/1.1 "Hop-by-Hop"."""
    return _hoppish(header_name.lower())

class ServerHandler:
    """S'occupe de l'invocation d'une application WSGI."""

    # Paramètres de configuration; il est possible de changer ces valeurs par
    # défaut en créant une nouvelle sous-classe ou en modifiant ces paramètres
    # à partir de l'instance.
    wsgi_version = (1,0)
    wsgi_multithread = True
    wsgi_multiprocess = True
    wsgi_run_once = False

    origin_server = True    # On transmet directement au client.
    http_version  = "1.0"   # La version du protocole HTTP qui doit être utilisée pour les réponses.
    server_software = software_version

    # os_environ permet d'accéder aux variables de configuration du système
    # d'exploitation sous-jacent : c'est par défaut une copie du map
    # 'os.environ' tel qu'il existe au moment de l'importation du module os
    # mais vous pouvez changer cela facilement, par exemple dans la méthode
    # __init__ d'une sous-classe.
    os_environ = dict(os.environ.items())


    # Classes amies.
    wsgi_file_wrapper = FileWrapper     # affectez None pour désactiver cette fonctionnalité
    headers_class = Headers             # doit être une classe semblable à Headers

    # Gestion des erreurs (là aussi, changez ces valeurs par défaut en créant
    # une sous-classe ou en modifiant directement l'instance).
    traceback_limit = None  # On affiche la trace de la pile dans sa totalité en passant par self.get_stderr().
    error_status = "500 Mec, y a un problème!"
    error_headers = [('Content-Type','text/plain')]

    # Variables relatives à l'état du serveur (ne changez pas celles-là).
    status = result = None
    headers_sent = False
    headers = None
    bytes_sent = 0

    def __init__(self, stdin, stdout, stderr, environ, multithread=True,
        multiprocess=False):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.base_env = environ
        self.wsgi_multithread = multithread
        self.wsgi_multiprocess = multiprocess

    def run(self, application):
        """Invoque l'application."""
        # Note : ne pas déplacer l'appel close()! Les serveurs asynchrones ne
        # doivent pas appeler close() à partir de finish_response(), et donc si
        # vous déplacez close() ailleurs que la deuxième clause except, vous
        # empêcherez les serveurs asynchrones de fonctionner en fermant
        # prématurément. Les serveurs asynchrones doivent sortir de 'run()'
        # sans avoir appelé close(), au cas où il y ait encore des données sur
        # lesquelles itérer.
        try:
            self.setup_environ()
            self.result = application(self.environ, self.start_response)
            self.finish_response()
        except:
            try:
                self.handle_error()
            except:
                # Si une erreur se déclenche alors qu'on essaie justement de
                # gérer une erreur, on abandonne!
                self.close()
                raise   # ...et on laisse au serveur le soin de se charger de tout cela.

    def setup_environ(self):
        """Configure l'environnement pour une requête donnée."""

        env = self.environ = self.os_environ.copy()
        self.add_cgi_vars()

        env['wsgi.input']        = self.get_stdin()
        env['wsgi.errors']       = self.get_stderr()
        env['wsgi.version']      = self.wsgi_version
        env['wsgi.run_once']     = self.wsgi_run_once
        env['wsgi.url_scheme']   = self.get_scheme()
        env['wsgi.multithread']  = self.wsgi_multithread
        env['wsgi.multiprocess'] = self.wsgi_multiprocess

        if self.wsgi_file_wrapper is not None:
            env['wsgi.file_wrapper'] = self.wsgi_file_wrapper

        if self.origin_server and self.server_software:
            env.setdefault('SERVER_SOFTWARE',self.server_software)

    def finish_response(self):
        """Envoie toutes les données présentes dans l'objet itérable renvoyé par
        l'application puis ferme self ainsi que l'objet itérable.

        Les sous-classes destinées à être employées au sein d'un serveur
        asynchrone devront redéfinir cette méthode de manière à ce qu'elle
        définisse une fonction particulière à appeler afin d'itérer sur les
        données de l'objet itérable et appeler 'self.close()' une fois le
        traitement de la réponse terminé.
        """
        if not self.result_is_file() and not self.sendfile():
            for data in self.result:
                self.write(data)
            self.finish_content()
        self.close()

    def get_scheme(self):
        """Renvoie le schéma d'URL à utiliser."""
        return guess_scheme(self.environ)

    def set_content_length(self):
        """Calcule l'en-tête Content-Length ou utilise si possible le transfert
        par morceaux."""
        try:
            blocks = len(self.result)
        except (TypeError,AttributeError,NotImplementedError):
            pass
        else:
            if blocks==1:
                self.headers['Content-Length'] = str(self.bytes_sent)
                return
        # XXX Ajouter le transfert par morceaux si le serveur est d'origine et si
        # le client prend en charge HTTP 1.1

    def cleanup_headers(self):
        """Apporte tout changement nécessaire aux en-têtes et applique les
        valeurs par défaut.

        Les sous-classes peuvent étendre cette méthode pour ajouter leurs
        propres valeurs par défaut.
        """
        if not self.headers.has_key('Content-Length'):
            self.set_content_length()

    def start_response(self, status, headers,exc_info=None):
        """Objet appelable 'start_response', conforme à la définition donnée par
        le PEP 333."""

        if exc_info:
            try:
                if self.headers_sent:
                    # On re-déclenche l'exception d'origine si les en-têtes ont
                    # déjà été envoyés.
                    raise exc_info[0], exc_info[1], exc_info[2]
            finally:
                exc_info = None        # permet d'éviter l'emploi d'une référence circulaire errante
        elif self.headers is not None:
            raise AssertionError("Les en-têtes ont déjà été définis!")

        assert type(status) is StringType, "L'argument status doit être une chaîne."
        assert len(status)>=4, "L'argument status doit comporter au moins 4 caractères."
        assert int(status[:3]), "L'argument status doit commencer par un code à 3 chiffres."
        assert status[3]==" ", "L'argument status doit comporter un espace après le code à 3 chiffres."
        if __debug__:
            for name,val in headers:
                assert type(name) is StringType, "Les noms d'en-tête doivent être des chaînes."
                assert type(val) is StringType, "Les valeurs d'en-tête doivent être des chaînes."
                assert not is_hop_by_hop(name), "Les en-têtes hop-by-hop sont interdits."
        self.status = status
        self.headers = self.headers_class(headers)
        return self.write

    def send_preamble(self):
        """Transmet les informations version/état/date/serveur au client, via
        self._write()."""
        if self.origin_server:
            if self.client_is_modern():
                self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
                if not self.headers.has_key('Date'):
                    self._write(
                        'Date: %s\r\n' % time.asctime(time.gmtime(time.time()))
                    )
                if self.server_software and not self.headers.has_key('Server'):
                    self._write('Server: %s\r\n' % self.server_software)
        else:
            self._write('Status: %s\r\n' % self.status)

    def write(self, data):
        """Objet appelable 'write', conforme à la définition donnée par le PEP
        333."""

        assert type(data) is StringType, "L'argument passé à write() doit être une chaîne."

        if not self.status:
             raise AssertionError("L'objet appelable write a été appelé avant l'objet appelable start_response.")

        elif not self.headers_sent:
            # Avant de s'occuper de la première chaîne à envoyer en sortie, on envoie
            # les en-têtes déjà stockés.
            self.bytes_sent = len(data)    # on fait en sorte de connaître content-length
            self.send_headers()
        else:
            self.bytes_sent += len(data)

        # XXX doit-on vérifier Content-Length et tronquer les données si trop
        # d'octets ont été écrits?
        self._write(data)
        self._flush()

    def sendfile(self):
        """Procédé de transmission de fichier spécifique de la plate-forme
        utilisée.

        Surchargez cette méthode dans les sous-classes afin de prendre en
        charge des procédés de transmission de fichier spécifiques de la
        plate-forme utilisée. Cette méthode sera appelée uniquement si l'objet
        itérable renvoyé par l'application ('self.result') est une instance de
        'self.wsgi_file_wrapper'.

        Cette méthode doit renvoyer une valeur vraie si il a été possible de
        transmettre l'objet ayant une interface semblable aux objets fichier à
        l'aide d'un procédé de transmission spécifique de la plate-forme
        utilisée. Elle doit renvoyer une valeur fausse si l'itération normale
        doit être utilisée à la place. Une exception peut être déclenchée pour
        indiquer qu'une transmission a été tentée mais a échoué.

        NOTE : cette méthode doit appeler 'self.send_headers()' si le flag
        'self.headers_sent' est inactif et elle doit tenter une transmission
        directe du fichier.
        """
        return False   # Pas de transmission de fichier spécifique de la plate-forme utilisée par défaut.

    def finish_content(self):
        """S'assure que les en-têtes et le contenu ont été envoyés."""
        if not self.headers_sent:
            self.headers['Content-Length'] = "0"
            self.send_headers()
        else:
            pass # doit-on vérifier si Content-Length est trop court?

    def close(self):
        try:
            self.request_handler.log_request(self.status.split(' ',1)[0], self.bytes_sent)
        finally:
            try:
                if hasattr(self.result,'close'):
                    self.result.close()
            finally:
                self.result = self.headers = self.status = self.environ = None
                self.bytes_sent = 0; self.headers_sent = False

    def send_headers(self):
        """Transmet les en-têtes au client, via self._write()."""
        self.cleanup_headers()
        self.headers_sent = True
        if not self.origin_server or self.client_is_modern():
            self.send_preamble()
            self._write(str(self.headers))

    def result_is_file(self):
        """Renvoie True si 'self.result' est une instance de 'self.wsgi_file_wrapper'."""
        wrapper = self.wsgi_file_wrapper
        return wrapper is not None and isinstance(self.result,wrapper)

    def client_is_modern(self):
        """Renvoie True si le client peut accepter un code d'état et des en-têtes."""
        return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'

    def log_exception(self,exc_info):
        """Inscrit dans le fichier log du serveur le tuple 'exc_info'.

        Les sous-classes peuvent surcharger cette méthode pour utiliser un
        autre fichier log ou modifier le format d'affichage du tuple dans le
        fichier log.
        """
        try:
            from traceback import print_exception
            stderr = self.get_stderr()
            print_exception(
                exc_info[0], exc_info[1], exc_info[2],
                self.traceback_limit, stderr
            )
            stderr.flush()
        finally:
            exc_info = None

    def handle_error(self):
        """Inscrit dans un fichier log l'erreur actuelle et envoie le résultat
        de cette erreur au client, si possible."""
        self.log_exception(sys.exc_info())
        if not self.headers_sent:
            self.result = self.error_output(self.environ, self.start_response)
            self.finish_response()
        # XXX else: implémenter des techniques avancées permettant de récupérer
        # d'une erreur concernant HTML ou du texte?

    def error_output(self, environ, start_response):
        import traceback
        start_response(self.error_status, self.error_headers[:], sys.exc_info())
        return ['\n'.join(traceback.format_exception(*sys.exc_info()))]

    # Méthodes abstraites par excellence; *doivent* être surchargées par les
    # sous-classes.

    def _write(self,data):
        self.stdout.write(data)
        self._write = self.stdout.write

    def _flush(self):
        self.stdout.flush()
        self._flush = self.stdout.flush

    def get_stdin(self):
        return self.stdin

    def get_stderr(self):
        return self.stderr

    def add_cgi_vars(self):
        self.environ.update(self.base_env)

class WSGIServer(HTTPServer):
    """Serveur BaseHTTPServer qui implémente le protocole Python WSGI."""
    application = None

    def server_bind(self):
        """On surcharge server_bind afin de stocker le nom du serveur et
        d'autres informations."""
        try:
            HTTPServer.server_bind(self)
        except Exception, e:
            raise WSGIServerException, e
        self.setup_environ()

    def setup_environ(self):
        # On configure l'environnement de base.
        env = self.base_environ = {}
        env['SERVER_NAME'] = self.server_name
        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
        env['SERVER_PORT'] = str(self.server_port)
        env['REMOTE_HOST']=''
        env['CONTENT_LENGTH']=''
        env['SCRIPT_NAME'] = ''

    def get_app(self):
        return self.application

    def set_app(self,application):
        self.application = application

class WSGIRequestHandler(BaseHTTPRequestHandler):
    server_version = "WSGIServer/" + __version__
    def get_environ(self):
        env = self.server.base_environ.copy()
        env['SERVER_PROTOCOL'] = self.request_version
        env['REQUEST_METHOD'] = self.command
        if '?' in self.path:
            path,query = self.path.split('?',1)
        else:
            path,query = self.path,''

        env['PATH_INFO'] = urllib.unquote(path)
        env['QUERY_STRING'] = query

        host = self.address_string()
        if host != self.client_address[0]:
            env['REMOTE_HOST'] = host
        env['REMOTE_ADDR'] = self.client_address[0]

        if self.headers.typeheader is None:
            env['CONTENT_TYPE'] = self.headers.type
        else:
            env['CONTENT_TYPE'] = self.headers.typeheader

        length = self.headers.getheader('content-length')
        if length:
            env['CONTENT_LENGTH'] = length

        for h in self.headers.headers:
            k,v = h.split(':',1)
            k=k.replace('-','_').upper(); v=v.strip()
            if k in env:
                continue                    # on saute les en-têtes Content-Length, Content-Type...
            if 'HTTP_'+k in env:
                env['HTTP_'+k] += ','+v     # les valeurs pour les en-têtes multiples sont séparées par des virgules
            else:
                env['HTTP_'+k] = v
        return env

    def get_stderr(self):
        return sys.stderr

    def handle(self):
        """Traite une requête HTTP."""
        self.raw_requestline = self.rfile.readline()
        if not self.parse_request(): # Un code d'erreur a été généré, on sort de la méthode.
            return
        handler = ServerHandler(self.rfile, self.wfile, self.get_stderr(), self.get_environ())
        handler.request_handler = self      # pointeur pour pouvoir écrire dans le fichier log
        handler.run(self.server.get_app())

def run(port, wsgi_handler):
    server_address = ('', port)
    httpd = WSGIServer(server_address, WSGIRequestHandler)
    httpd.set_app(wsgi_handler)
    httpd.serve_forever()
